// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/renderer/devtools/devtools_agent.h"

#include <stddef.h>

#include <map>

#include "base/json/json_writer.h"
#include "base/lazy_instance.h"
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/threading/non_thread_safe.h"
#include "base/trace_event/trace_event.h"
#include "content/common/devtools_messages.h"
#include "content/common/frame_messages.h"
#include "content/public/common/manifest.h"
#include "content/renderer/devtools/devtools_client.h"
#include "content/renderer/devtools/devtools_cpu_throttler.h"
#include "content/renderer/manifest/manifest_manager.h"
#include "content/renderer/render_frame_impl.h"
#include "content/renderer/render_widget.h"
#include "ipc/ipc_channel.h"
#include "third_party/WebKit/public/platform/WebFloatRect.h"
#include "third_party/WebKit/public/platform/WebPoint.h"
#include "third_party/WebKit/public/platform/WebString.h"
#include "third_party/WebKit/public/web/WebDevToolsAgent.h"
#include "third_party/WebKit/public/web/WebLocalFrame.h"

using blink::WebDevToolsAgent;
using blink::WebDevToolsAgentClient;
using blink::WebLocalFrame;
using blink::WebPoint;
using blink::WebString;

using base::trace_event::TraceLog;

namespace content {

namespace {

    const size_t kMaxMessageChunkSize = IPC::Channel::kMaximumMessageSize / 4;
    const char kPageGetAppManifest[] = "Page.getAppManifest";

    class WebKitClientMessageLoopImpl
        : public WebDevToolsAgentClient::WebKitClientMessageLoop,
          public base::NonThreadSafe {
    public:
        WebKitClientMessageLoopImpl() = default;
        ~WebKitClientMessageLoopImpl() override { DCHECK(CalledOnValidThread()); }
        void run() override
        {
            DCHECK(CalledOnValidThread());

            base::RunLoop* const previous_run_loop = run_loop_;
            base::RunLoop run_loop;
            run_loop_ = &run_loop;

            base::MessageLoop::ScopedNestableTaskAllower allow(
                base::MessageLoop::current());
            run_loop.Run();

            run_loop_ = previous_run_loop;
        }
        void quitNow() override
        {
            DCHECK(CalledOnValidThread());
            DCHECK(run_loop_);

            run_loop_->Quit();
        }

    private:
        base::RunLoop* run_loop_ = nullptr;
    };

    typedef std::map<int, DevToolsAgent*> IdToAgentMap;
    base::LazyInstance<IdToAgentMap>::Leaky
        g_agent_for_routing_id
        = LAZY_INSTANCE_INITIALIZER;

} //  namespace

DevToolsAgent::DevToolsAgent(RenderFrameImpl* frame)
    : RenderFrameObserver(frame)
    , is_attached_(false)
    , is_devtools_client_(false)
    , paused_(false)
    , frame_(frame)
    , cpu_throttler_(new DevToolsCPUThrottler())
    , weak_factory_(this)
{
    g_agent_for_routing_id.Get()[routing_id()] = this;
    frame_->GetWebFrame()->setDevToolsAgentClient(this);
}

DevToolsAgent::~DevToolsAgent()
{
    g_agent_for_routing_id.Get().erase(routing_id());
}

// Called on the Renderer thread.
bool DevToolsAgent::OnMessageReceived(const IPC::Message& message)
{
    bool handled = true;
    IPC_BEGIN_MESSAGE_MAP(DevToolsAgent, message)
    IPC_MESSAGE_HANDLER(DevToolsAgentMsg_Attach, OnAttach)
    IPC_MESSAGE_HANDLER(DevToolsAgentMsg_Reattach, OnReattach)
    IPC_MESSAGE_HANDLER(DevToolsAgentMsg_Detach, OnDetach)
    IPC_MESSAGE_HANDLER(DevToolsAgentMsg_DispatchOnInspectorBackend,
        OnDispatchOnInspectorBackend)
    IPC_MESSAGE_HANDLER(DevToolsAgentMsg_InspectElement, OnInspectElement)
    IPC_MESSAGE_HANDLER(DevToolsAgentMsg_RequestNewWindow_ACK,
        OnRequestNewWindowACK)
    IPC_MESSAGE_HANDLER(DevToolsMsg_SetupDevToolsClient, OnSetupDevToolsClient)
    IPC_MESSAGE_UNHANDLED(handled = false)
    IPC_END_MESSAGE_MAP()

    if (message.type() == FrameMsg_Navigate::ID)
        ContinueProgram(); // Don't want to swallow the message.

    return handled;
}

void DevToolsAgent::WidgetWillClose()
{
    ContinueProgram();
}

void DevToolsAgent::OnDestruct()
{
    delete this;
}

void DevToolsAgent::sendProtocolMessage(int session_id,
    int call_id,
    const blink::WebString& message,
    const blink::WebString& state_cookie)
{
    if (!send_protocol_message_callback_for_test_.is_null()) {
        send_protocol_message_callback_for_test_.Run(
            session_id, call_id, message.utf8(), state_cookie.utf8());
        return;
    }
    SendChunkedProtocolMessage(this, routing_id(), session_id, call_id,
        message.utf8(), state_cookie.utf8());
}

// static
blink::WebDevToolsAgentClient::WebKitClientMessageLoop*
DevToolsAgent::createMessageLoopWrapper()
{
    return new WebKitClientMessageLoopImpl();
}

blink::WebDevToolsAgentClient::WebKitClientMessageLoop*
DevToolsAgent::createClientMessageLoop()
{
    return createMessageLoopWrapper();
}

void DevToolsAgent::willEnterDebugLoop()
{
    paused_ = true;
}

void DevToolsAgent::didExitDebugLoop()
{
    paused_ = false;
}

bool DevToolsAgent::requestDevToolsForFrame(blink::WebLocalFrame* webFrame)
{
    RenderFrameImpl* frame = RenderFrameImpl::FromWebFrame(webFrame);
    if (!frame)
        return false;
    Send(new DevToolsAgentHostMsg_RequestNewWindow(routing_id(),
        frame->GetRoutingID()));
    return true;
}

void DevToolsAgent::enableTracing(const WebString& category_filter)
{
    // Tracing is already started by DevTools TracingHandler::Start for the
    // renderer target in the browser process. It will eventually start tracing in
    // the renderer process via IPC. But we still need a redundant
    // TraceLog::SetEnabled call here for
    // InspectorTracingAgent::emitMetadataEvents(), at which point, we are not
    // sure if tracing is already started in the renderer process.
    TraceLog* trace_log = TraceLog::GetInstance();
    trace_log->SetEnabled(
        base::trace_event::TraceConfig(category_filter.utf8(), ""),
        TraceLog::RECORDING_MODE);
}

void DevToolsAgent::disableTracing()
{
    TraceLog::GetInstance()->SetDisabled();
}

void DevToolsAgent::setCPUThrottlingRate(double rate)
{
    cpu_throttler_->SetThrottlingRate(rate);
}

// static
DevToolsAgent* DevToolsAgent::FromRoutingId(int routing_id)
{
    IdToAgentMap::iterator it = g_agent_for_routing_id.Get().find(routing_id);
    if (it != g_agent_for_routing_id.Get().end()) {
        return it->second;
    }
    return NULL;
}

// static
void DevToolsAgent::SendChunkedProtocolMessage(IPC::Sender* sender,
    int routing_id,
    int session_id,
    int call_id,
    const std::string& message,
    const std::string& post_state)
{
    DevToolsMessageChunk chunk;
    chunk.message_size = message.size();
    chunk.is_first = true;

    if (message.length() < kMaxMessageChunkSize) {
        chunk.data = message;
        chunk.session_id = session_id;
        chunk.call_id = call_id;
        chunk.post_state = post_state;
        chunk.is_last = true;
        sender->Send(new DevToolsClientMsg_DispatchOnInspectorFrontend(
            routing_id, chunk));
        return;
    }

    for (size_t pos = 0; pos < message.length(); pos += kMaxMessageChunkSize) {
        chunk.is_last = pos + kMaxMessageChunkSize >= message.length();
        chunk.session_id = chunk.is_last ? session_id : 0;
        chunk.call_id = chunk.is_last ? call_id : 0;
        chunk.post_state = chunk.is_last ? post_state : std::string();
        chunk.data = message.substr(pos, kMaxMessageChunkSize);
        sender->Send(new DevToolsClientMsg_DispatchOnInspectorFrontend(
            routing_id, chunk));
        chunk.is_first = false;
        chunk.message_size = 0;
    }
}

void DevToolsAgent::OnAttach(const std::string& host_id, int session_id)
{
    GetWebAgent()->attach(WebString::fromUTF8(host_id), session_id);
    is_attached_ = true;
}

void DevToolsAgent::OnReattach(const std::string& host_id,
    int session_id,
    const std::string& agent_state)
{
    GetWebAgent()->reattach(WebString::fromUTF8(host_id), session_id,
        WebString::fromUTF8(agent_state));
    is_attached_ = true;
}

void DevToolsAgent::OnDetach()
{
    GetWebAgent()->detach();
    is_attached_ = false;
}

void DevToolsAgent::OnDispatchOnInspectorBackend(int session_id,
    int call_id,
    const std::string& method,
    const std::string& message)
{
    TRACE_EVENT0("devtools", "DevToolsAgent::OnDispatchOnInspectorBackend");
    if (method == kPageGetAppManifest) {
        ManifestManager* manager = frame_->manifest_manager();
        manager->GetManifest(
            base::Bind(&DevToolsAgent::GotManifest,
                weak_factory_.GetWeakPtr(), session_id, call_id));
        return;
    }
    GetWebAgent()->dispatchOnInspectorBackend(session_id,
        call_id,
        WebString::fromUTF8(method),
        WebString::fromUTF8(message));
}

void DevToolsAgent::OnInspectElement(int session_id, int x, int y)
{
    blink::WebFloatRect point_rect(x, y, 0, 0);
    frame_->GetRenderWidget()->convertWindowToViewport(&point_rect);
    GetWebAgent()->inspectElementAt(
        session_id, WebPoint(point_rect.x, point_rect.y));
}

void DevToolsAgent::OnRequestNewWindowACK(bool success)
{
    if (!success)
        GetWebAgent()->failedToRequestDevTools();
}

void DevToolsAgent::ContinueProgram()
{
    GetWebAgent()->continueProgram();
}

void DevToolsAgent::OnSetupDevToolsClient(
    const std::string& compatibility_script)
{
    // We only want to register once; and only in main frame.
    DCHECK(!frame_->GetWebFrame()->parent());
    if (is_devtools_client_)
        return;
    is_devtools_client_ = true;
    new DevToolsClient(frame_, compatibility_script);
}

WebDevToolsAgent* DevToolsAgent::GetWebAgent()
{
    return frame_->GetWebFrame()->devToolsAgent();
}

bool DevToolsAgent::IsAttached()
{
    return is_attached_;
}

void DevToolsAgent::GotManifest(int session_id,
    int call_id,
    const GURL& manifest_url,
    const Manifest& manifest,
    const ManifestDebugInfo& debug_info)
{
    if (!is_attached_)
        return;

    std::unique_ptr<base::DictionaryValue> response(new base::DictionaryValue());
    response->SetInteger("id", call_id);
    std::unique_ptr<base::DictionaryValue> result(new base::DictionaryValue());
    std::unique_ptr<base::ListValue> errors(new base::ListValue());

    bool failed = false;
    for (const auto& error : debug_info.errors) {
        std::unique_ptr<base::DictionaryValue> error_value(
            new base::DictionaryValue());
        error_value->SetString("message", error.message);
        error_value->SetBoolean("critical", error.critical);
        error_value->SetInteger("line", error.line);
        error_value->SetInteger("column", error.column);
        if (error.critical)
            failed = true;
        errors->Append(std::move(error_value));
    }

    WebString url = frame_->GetWebFrame()->document().manifestURL().string();
    result->SetString("url", url);
    if (!failed)
        result->SetString("data", debug_info.raw_data);
    result->Set("errors", errors.release());
    response->Set("result", result.release());

    std::string json_message;
    base::JSONWriter::Write(*response, &json_message);
    SendChunkedProtocolMessage(this, routing_id(), session_id, call_id,
        json_message, std::string());
}

} // namespace content
